/**
 * 时间：2024.04
 * 编译：make
 * 执行: ./multi_download https://releases.ubuntu.com/20.04/ubuntu-20.04.6-desktop-amd64.iso.zsync ubuntu.zsync
 * 该文件代码需要多线程下载 且 中断下载保存下载进度
 * 1. 如何断点续传
 *  采用文件保存进度，如果有文件，需要判断进程是否下载完毕（看开始下载的位置offset是否大于最后的位置end）
 * 2. 续传百分比第二段如何达到100%
 *  额外计算出续传所需要下载的大小。
 * 3. 每次g++编译过于麻烦
 *  需要写一个makefile
 */
#include <bits/stdc++.h>
#include <curl/curl.h> // libcurl中的头文件
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <pthread.h>
#include <signal.h>
using namespace std;

#define THREAD_NUM 10
struct fileInfo
{
    const char *url;
    char *fileptr;
    int offset; // 每个线程开始位置
    int end;    // 每个线程结束位置
    pthread_t thid;
    double download; // 每个线程下载的数量
    double totalDownload; // 每个线程已下载
    FILE *recordfile;
};
struct fileInfo **pInfoTbl;
// 下载文件的长度
double downloadFileLength = 0;

size_t writeFunc(void *ptr, size_t size, size_t memb, void *userdata)
{
    // cout << "WriteFunc: " << size * memb << endl;
    struct fileInfo *info = (struct fileInfo *)userdata;
    // 按偏移写入数据
    memcpy(info->fileptr + info->offset, ptr, size * memb);
    info->offset += size * memb;
    return size * memb;
}

// 请求文件大小
double getDownloadFileLength(const char *url)
{
    // 初始化
    CURL *curl = curl_easy_init();
    // 只发送HTTP头部
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "MyCustomClient/1.0");
    curl_easy_setopt(curl, CURLOPT_HEADER, 1);
    curl_easy_setopt(curl, CURLOPT_NOBODY, 1);

    // 执行网络连接，建立TCP连接，等待HTTP请求，等待数据返回，循环等待数据读取完毕
    CURLcode res = curl_easy_perform(curl);
    if (res == CURLE_OK)
    {
        curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &downloadFileLength);
    }
    else
    {
        downloadFileLength = -1;
    }
    // 清空上下文
    curl_easy_cleanup(curl);
    return downloadFileLength;
}

int progressFunc(void *userdata, double totalDownload, double nowDownload, double totalUpload, double nowUpload)
{
    int percent = 0;
    static int print = 1;
    struct fileInfo *info = (struct fileInfo *)userdata;
    info->download = nowDownload;
    info->totalDownload = totalDownload;
    if (totalDownload > 0)
    {
        double allDownload = 0;
        double total = 0;
        for (int i = 0; i <= THREAD_NUM; i++)
        {
            allDownload += pInfoTbl[i]->download;
            total +=pInfoTbl[i]->totalDownload;
        }
        percent = (int)(allDownload / total * 100);
    }
    if (percent == print)
    {
        printf("threadid: %ld, percent: %d%%\n", info->thid, percent);
        print += 1;
    }
    return 0;
}

void *worker(void *arg)
{
    struct fileInfo *info = (struct fileInfo *)arg;

    char range[64] = {0};
    // 如果存在存储进去的文件
    if (info->recordfile)
    {
        fscanf(info->recordfile, "%d-%d", &info->offset, &info->end);
    }
    // 用于检测是否下载完成
    if (info->offset > info->end)
        return NULL;
    snprintf(range, 64, "%d-%d", info->offset, info->end);

    cout << "thread_id: " << info->thid << ", download from:" << info->offset << " to: " << info->end << endl;
    // 初始化
    CURL *curl = curl_easy_init();
    // 设置参数url，回调函数，以及回调函数的携带的信息
    curl_easy_setopt(curl, CURLOPT_URL, info->url);           // 设置没有下载进度为0，即需要下载进度
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunc); // 保存本地
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, info);          // 保存本地的参数
    curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);           // 设置需要下载进度
    curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progressFunc);
    curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, info);
    curl_easy_setopt(curl, CURLOPT_RANGE, range);

    // 执行网络连接，建立TCP连接，发起HTTP请求，接收数据，循环执行回调函数writeFunc
    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK)
    {
        cout << "res: " << res << endl;
    }
    // 清空上下文
    curl_easy_cleanup(curl);

    return nullptr;
}

int download(const char *url, const char *filename)
{
    // 请求文件大小
    long fileLength = getDownloadFileLength(url);
    cout << "文件大小为：" << fileLength << endl;

    // 创建文件 需要头文件 #include <fcntl.h>
    // S_IRUSR | S_IWUSR 改变文件的权限
    int fd = open(filename, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1)
        return -1;
    // 开辟新的文件的大小，在末尾-1放置一个空格字符
    if (lseek(fd, fileLength - 1, SEEK_SET) == -1)
    {
        perror("lseek");
        close(fd);
        return -1;
    }
    if (write(fd, "", 1) != 1)
    {
        perror("write");
        close(fd);
        return -1;
    }
    char *fileptr = (char *)mmap(NULL, fileLength, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (fileptr == MAP_FAILED)
    {
        perror("mmap");
        close(fd);
        return -1;
    }

    FILE *fp = fopen(".tmp", "r");

    // 准备每个线程的参数，开辟THREAD_NUM + 1个线程
    int i = 0;
    long partSize = fileLength / THREAD_NUM;
    struct fileInfo *info[THREAD_NUM + 1] = {NULL};
    for (i = 0; i <= THREAD_NUM; i++)
    {
        // pthread_create(&thread_id[i], NULL, worker, fileptr);
        info[i] = (struct fileInfo *)malloc(sizeof(struct fileInfo));
        memset(info[i], 0, sizeof(struct fileInfo));

        info[i]->offset = i * partSize;
        if (i < THREAD_NUM)
            info[i]->end = (i + 1) * partSize - 1;
        else
            info[i]->end = fileLength - 1;
        info[i]->fileptr = fileptr;
        info[i]->url = url;
        info[i]->download = 0;
        info[i]->recordfile = fp;
    }
    pInfoTbl = info;

    // 创建线程
    // pthread_t thread_id[THREAD_NUM + 1] = {0};
    for (i = 0; i <= THREAD_NUM; i++)
    {
        // 线程id，线程属性，线程入口函数，传到子线程的参数
        pthread_create(&(info[i]->thid), NULL, worker, info[i]);
        // 线程会一次性创建，调度执行是并发的
        usleep(1);
    }
    // 同步
    for (i = 0; i <= THREAD_NUM; i++)
    {
        // 线程id，子线程返回的参数
        pthread_join(info[i]->thid, NULL);
    }

    // 手动释放malloc分配的内存，关闭mmap，关闭fd
    // 为什么不能和上面的同步线程合并？
    for (i = 0; i <= THREAD_NUM; i++)
    {
        free(info[i]);
    }
    fclose(fp);
    munmap(fileptr, fileLength);
    close(fd);
    return 0;
}

// 进程异常退出处理
void signal_handle(int signum)
{
    cout << "signum: " << signum << endl;
    int fd = open("./.tmp", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1)
        return;
    for (int i = 0; i <= THREAD_NUM; i++)
    {
        char down[64] = {0};
        snprintf(down, 64, "%d-%d\r\n", pInfoTbl[i]->offset, pInfoTbl[i]->end);
        write(fd, down, strlen(down));
    }
    exit(-1);
}

// 执行：multi_download 下载链接 保存的文件名
#if 1
int main(int argc, const char *argv[])
{
    if (argc != 3)
    {
        cout << "请执行: multi_download 下载链接 保存的文件名" << endl;
        return -1;
    }
    // 需要添加头文件 #include <signal.h>
    if (signal(SIGINT, signal_handle) == SIG_ERR)
    {
        cout << "signal error" << endl;
        return -1;
    }
    download(argv[1], argv[2]);
    return 0;
}
#endif